package com.edu.shiro.shiroconfig;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.servlet.Filter;

import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.session.mgt.eis.SessionIdGenerator;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.edu.shiro.filter.KickoutSessionControlFilter;
import com.edu.shiro.session.ShiroSessionListener;


@Configuration
public class ShiroConfig {
	
	/**
	 * ShiroFilterFactoryBean 处理拦截资源文件问题。
	 * 注意：初始化ShiroFilterFactoryBean的时候需要注入：SecurityManager
	 * Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截
	 * @param securityManager
	 * @return
	 */
	@Bean
	public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		shiroFilterFactoryBean.setSecurityManager(securityManager);
		
		//自定义拦截器限制并发人数
		LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
		//限制同一帐号同时在线的个数
		filtersMap.put("kickout", kickoutSessionControlFilter());
		shiroFilterFactoryBean.setFilters(filtersMap);
		
		Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
		// 过滤链定义，从上向下顺序执行，一般将 /**放在最为下边 --> : 这是一个坑，一不小心代码就不好使了
        //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了，登出后跳转配置的loginUrl
        // 配置不会被拦截的链接 顺序判断
		filterChainDefinitionMap.put("/logout", "logout");
		filterChainDefinitionMap.put("/static/**", "anon");
		filterChainDefinitionMap.put("/favicon.ico**", "anon");
		filterChainDefinitionMap.put("/login", "anon");
		//其他资源都需要认证  authc 表示需要认证才能进行访问 user表示配置记住我或认证通过可以访问的地址
		//表示 访问/**下的资源 首先要通过 kickout 后面的filter，然后再通过user后面对应的filter才可以访问。
		filterChainDefinitionMap.put("/**", "kickout,user");
		//配置shiro默认登录界面地址，前后端分离中登录界面跳转应由前端路由控制，后台仅返回json数据
		shiroFilterFactoryBean.setLoginUrl("/login");
		// 登录成功后要跳转的链接
		shiroFilterFactoryBean.setSuccessUrl("/index");
		//未授权界面;
		shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized.html");
		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
	
		return shiroFilterFactoryBean;
	}
	
	/**
	 * 用户数据注入
	 */
	@Bean
	public UserRealm userRealm(){
		UserRealm userRealm = new UserRealm();
		//开启缓存
//		userRealm.setCachingEnabled(true);
//		//启用身份验证缓存，即缓存AuthenticationInfo信息，默认false
//		userRealm.setAuthenticationCachingEnabled(true);
//		//缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置
//		userRealm.setAuthenticationCacheName("authenticationCache");
//		//启用授权缓存，即缓存AuthorizationInfo信息，默认false
//		userRealm.setAuthorizationCachingEnabled(true);
//		 //缓存AuthorizationInfo信息的缓存名称  在ehcache-shiro.xml中有对应缓存的配置
//		userRealm.setAuthorizationCacheName("authorizationCache");
//		
		//配置自定义密码比较器
		userRealm.setCredentialsMatcher(retryLimitHashedCredentialsMatcher());
		return userRealm;
	}
	
	/**
	 * 配置管理层，即安全控制层
	 * 
	 */
	@Bean
	public SecurityManager securityManager(){
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		securityManager.setRealm(userRealm());
		//配置ehcache缓存管理器
		securityManager.setCacheManager(ehCacheManager());
		//配置记住我
		securityManager.setRememberMeManager(rememberMeManager());
		//配置自定义session管理，使用ehcache或redis
		securityManager.setSessionManager(sessionManager());
		return securityManager;
	}
	
	/**
	 *  开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
	 * 	配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
	 * 	@return
	 * spring aop扫描
	 */
//	@Bean
//	public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
//		DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
//		advisorAutoProxyCreator.setProxyTargetClass(true);
//		return advisorAutoProxyCreator;
//	}
	
	 /**
	 * 开启shiro aop注解支持 使用代理方式所以需要开启代码支持
	 *  一定要写入上面advisorAutoProxyCreator（）自动代理。不然AOP注解不会生效
	 * @param securityManager
	 * @return
	 */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
		return authorizationAttributeSourceAdvisor;
	}
	
	/**
	 * 注册全局捕获异常
	 * 
	 */
//	@Bean(name = "exceptionHandler")
//	public HandlerExceptionResolver handlerExceptionResolver(){
//		return new GlobalExceptionResolver();
//	}
	
	/**
	 * shiro缓存管理器;
	 * 需要添加到securityManager中
	 * shiro-ehcache实现
	 * 单点登录：多个应用系统中，用户只需要登录一次就可以访问所有相互信任的应用系统。
	 */
	@Bean
	public EhCacheManager ehCacheManager(){
		EhCacheManager ehCacheManager = new EhCacheManager();
		ehCacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
		return ehCacheManager;
	}
	
	/**
	 * cookie对象;会话Cookie模板 ,默认为: JSESSIONID 问题: 与SERVLET容器名冲突,重新定义为sid或rememberMe，自定义
	 * @return
	 */
	@Bean
	public SimpleCookie rememberMeCookie(){
		 //这个参数是cookie的名称，对应前端的checkbox的name = rememberMe
		SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
		//setcookie的httponly属性如果设为true的话，会增加对xss防护的安全系数。它有以下特点：

	    //setcookie()的第七个参数
	    //设为true后，只能通过http访问，javascript无法访问
	    //防止xss读取cookie
		simpleCookie.setHttpOnly(true);
		simpleCookie.setPath("/");
		//<!-- 记住我cookie生效时间30天 ,单位秒;-->
		simpleCookie.setMaxAge(2592000);
		return simpleCookie;
	}
	
	/**
	 * cookie管理对象；记住我的功能，rememberMe管理器
	 */
	@Bean
	public CookieRememberMeManager rememberMeManager(){
		CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
		cookieRememberMeManager.setCookie(rememberMeCookie());
		 //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
//		cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
		return cookieRememberMeManager;
	}
	
	/**
	 * 记住我过滤器
	 */
	@Bean
	public FormAuthenticationFilter formAuthenticationFilter(){
		FormAuthenticationFilter formAuthenticationFilter = new FormAuthenticationFilter();
		 //对应前端的checkbox的name = rememberMe
		formAuthenticationFilter.setRememberMeParam("rememberMe");
		return formAuthenticationFilter;
	}
	
	/**
	 * 配置sessoon监听
	 */
	@Bean("sessionListener")
	public ShiroSessionListener sessionListener(){
		ShiroSessionListener shiroSessionListener = new ShiroSessionListener();
		return shiroSessionListener;
	}
	
	/**
	 * 配置会话id生成器
	 */
	@Bean
	public SessionIdGenerator sessionIdGenerator(){
		return new JavaUuidSessionIdGenerator();
	}
	
	/**
	 * SessionDao的作用是为Session提供crud持久化的一个shiro组件
	 * MemorySessionDao 直接在内存中进行会话维护
	 * EnterpriseCacheSessionDao提供了缓存功能的会话维护，默认情况下使用mapCache实现
	 * 内部使用ConcurrentHashMap报存缓存的会话
	 */
	@Bean
	public SessionDAO sessionDAO(){
		EnterpriseCacheSessionDAO enterpriseCacheSessionDAO = new EnterpriseCacheSessionDAO();
		//使用ehCacheManager
		enterpriseCacheSessionDAO.setCacheManager(ehCacheManager());
		//设置session缓存的名字  默认为shiro-activeSessionCache
		enterpriseCacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
		//sessionID生成器
		enterpriseCacheSessionDAO.setSessionIdGenerator(sessionIdGenerator());
		return enterpriseCacheSessionDAO;
	}
	
	/**
	 * 配置保存sessionID的cookie
	 * 注意：这里的cookie不是上面记住我的cookie,记住我 需要一个cookie session管理 也需要自己的cookie
	 */
	@Bean("sessionIdCookie")
	public SimpleCookie sessionIdCookie(){
		//参数是cookie的名称
		SimpleCookie simpleCookie = new SimpleCookie("sid");
		 //setcookie的httponly属性如果设为true的话，会增加对xss防护的安全系数。它有以下特点：
	    //setcookie()的第七个参数
	    //设为true后，只能通过http访问，javascript无法访问
	    //防止xss读取cookie
		simpleCookie.setHttpOnly(true);
		simpleCookie.setPath("/");
		//maxAge=-1表示浏览器关闭时失效此Cookie
		simpleCookie.setMaxAge(-1);
		return simpleCookie;
	}
	
	
	/**
	 * 配置会话管理器，设定会话超时及保存
	 */
	@Bean("sessionManager")
	public SessionManager sessionManager(){
		DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
		Collection<SessionListener> listeners = new ArrayList<SessionListener>();
		listeners.add(sessionListener());
		//配置监听器
		sessionManager.setSessionListeners(listeners);
		sessionManager.setSessionIdCookie(sessionIdCookie());
		sessionManager.setSessionDAO(sessionDAO());
		sessionManager.setCacheManager(ehCacheManager());
		
		//配置清理孤立session
		//全局会话超时时间（单位毫秒），默认30分钟  暂时设定为10秒钟测试
		sessionManager.setGlobalSessionTimeout(1800000);
		//是否开启删除无效的session对象  默认是true
		sessionManager.setDeleteInvalidSessions(true);
		//是否开启定时调度器进行检测过期session默认为true;
		sessionManager.setSessionValidationSchedulerEnabled(true);
		//设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
	    //设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
	    //暂时设置为 5秒 用来测试
		sessionManager.setSessionValidationInterval(1800000);
		//取消url 后面的 JSESSIONID
	    sessionManager.setSessionIdUrlRewritingEnabled(false);
		return sessionManager;
	}
	
	/**
	 * 并发登录控制
	 */
	@Bean
	public KickoutSessionControlFilter kickoutSessionControlFilter(){
		KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
		//用于根据会话id,获取会话进行踢出操作
		kickoutSessionControlFilter.setSessionManager(sessionManager());
		//使用CacheManager获取相应的cache来缓存用户登录的会话；用于保存用户-会话之间的关系
		kickoutSessionControlFilter.setCache(ehCacheManager());
		//是否踢出后来登录的，默认false;即后者登录的用户踢出前者登录的用户
		kickoutSessionControlFilter.setKickoutAfter(false);
		//同一个用户最大会话数，默认1
		kickoutSessionControlFilter.setMaxSession(1);
		//被踢出后重定向的地址
		kickoutSessionControlFilter.setKickoutUrl("/login?kickout=1");
		return kickoutSessionControlFilter;
	}
	
	
	/**
	 * 配置密码比较器
	 */
	@Bean
	public RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher(){
		RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher(ehCacheManager());
		//如果密码加密,可以打开下面配置
	    //加密算法的名称
	    //retryLimitHashedCredentialsMatcher.setHashAlgorithmName("MD5");
	    //配置加密的次数
	    //retryLimitHashedCredentialsMatcher.setHashIterations(1024);
	    //是否存储为16进制
	    //retryLimitHashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
		return retryLimitHashedCredentialsMatcher;
	}
	
	

	
	
}
